/*=============================================================================
Copyright (c) 2010 Crytek Studios. All Rights Reserved.

Revision history:
* Created by Nicolas Schulz

=============================================================================*/

#include "StdAfx.h"
#include "D3DStereo.h"
#include "DriverD3D.h"
#include "D3DPostProcess.h"

#if (defined(WIN32) || defined(WIN64)) && !defined(gringo)
#include "IHardwareMouse.h"
#include "../Common/NVAPI/nvapi.h"
#endif


CD3DStereoRenderer::CD3DStereoRenderer(CD3D9Renderer& renderer, StereoDevice device)
  : m_renderer(renderer)
	, m_device(device)
	, m_deviceState(STEREO_DEVSTATE_UNSUPPORTED_DEVICE)
  , m_mode(STEREO_MODE_NO_STEREO)
  , m_output(STEREO_OUTPUT_STANDARD)
	, m_driver(DRIVER_UNKNOWN)
	, m_pLeftTex(NULL)
	, m_pRightTex(NULL)
	, m_nvStereoHandle(NULL)
	, m_nvStereoStrength(0)
	, m_nvStereoActivated(0)
	, m_curEye(LEFT_EYE)
	, m_frontBufWidth(0)
	, m_frontBufHeight(0)
	, m_gammaAdjustment(0)
	, m_resourcesPatched(false)
	, m_needClearLeft(true)
	, m_needClearRight(true)
{





	if( device == STEREO_DEVICE_DEFAULT )
		SelectDefaultDevice();
}


CD3DStereoRenderer::~CD3DStereoRenderer()
{
	Shutdown();
}


void CD3DStereoRenderer::SelectDefaultDevice()
{
	StereoDevice device = STEREO_DEVICE_NONE;

#if (defined(WIN32) || defined(WIN64)) && !defined(gringo)
	device = STEREO_DEVICE_DRIVER;




#endif

	m_device = device;
}


void CD3DStereoRenderer::InitDeviceBeforeD3D()
{
#ifdef USE_NV_API
	bool availableDriverNV = false;
	uint8 enabled = 0;
	
	// Always set direct mode so that the NV driver never does its own stereo
	if( NvAPI_Initialize() == NVAPI_OK &&
	    NvAPI_Stereo_SetDriverMode( NVAPI_STEREO_DRIVER_MODE_DIRECT ) == NVAPI_OK &&
	    NvAPI_Stereo_IsEnabled( &enabled ) == NVAPI_OK )
	{
		if( !enabled )
			iLog->LogWarning( "3DVision is disabled in control panel" );
		else
			availableDriverNV = true;
	}

	if( !availableDriverNV && m_device == STEREO_DEVICE_DRIVER && CRenderer::CV_r_StereoDevice == STEREO_DEVICE_DEFAULT )
	{
		// Fall back to frame-compatible device
		m_device = STEREO_DEVICE_FRAMECOMP;
	}
#endif

	if( m_device == STEREO_DEVICE_NONE )
		return;

	bool success = true;
	m_deviceState = STEREO_DEVSTATE_UNSUPPORTED_DEVICE;
		
#ifdef USE_NV_API
	if( m_device == STEREO_DEVICE_DRIVER )
	{
		if( availableDriverNV )
		{
			m_driver = DRIVER_NV;
		}
		else
		{
			success = false;
			m_deviceState = STEREO_DEVSTATE_BAD_DRIVER;
		}
	}
	else if( m_device == STEREO_DEVICE_DUALHEAD )
	{
		ICVar *cvar = iConsole->GetCVar("r_Fullscreen");
		bool fullscreen = gEnv->IsEditor() ? false : cvar && cvar->GetIVal() > 0;
		
		if( !fullscreen )
		{
			success = false;
			m_deviceState = STEREO_DEVSTATE_REQ_FULLSCREEN;
		}
	}
	else if( m_device == STEREO_DEVICE_HDMI )
	{
		success = false;
	}
































#endif

	if( success )
	{
		m_deviceState = STEREO_DEVSTATE_OK;
	}
	else
	{
		iLog->LogError( "Failed to create stereo device" );
		m_device = STEREO_DEVICE_NONE;
	}
}


void CD3DStereoRenderer::InitDeviceAfterD3D()
{
#ifdef USE_NV_API
	if( IsDriver( DRIVER_NV ) )
	{
		if( m_nvStereoHandle )
		{
			NvAPI_Stereo_DestroyHandle( m_nvStereoHandle );
			m_nvStereoHandle = NULL;
		}
		
		void *pStereoHandle;
		if( NvAPI_Stereo_CreateHandleFromIUnknown( m_renderer.m_pd3dDevice, &pStereoHandle ) == NVAPI_OK )
			m_nvStereoHandle = pStereoHandle;
		else
			m_device = STEREO_DEVICE_NONE;

		DisableStereo();  // Always disabled at startup
	}
#endif

	// Note: Resources will be created in EF_Init, so no need to create them here
}


void CD3DStereoRenderer::CreateResources()
{
	if( m_device == STEREO_DEVICE_NONE )
		return;
	
	m_SourceSizeParamName = CCryNameR("SourceSize");

#if !defined(XENON) && !defined(PS3)
	if( !CTexture::s_ptexStereoL || !CTexture::s_ptexStereoR )
	{
		CreateIntermediateBuffers();
	}
#endif
}


void CD3DStereoRenderer::CreateIntermediateBuffers()
{
	SAFE_RELEASE( CTexture::s_ptexStereoL );
	SAFE_RELEASE( CTexture::s_ptexStereoR );

	int nWidth = m_renderer.GetWidth();
	int nHeight = m_renderer.GetHeight();
  uint32 nFlags = FT_DONT_STREAM | FT_USAGE_RENDERTARGET | FT_USAGE_PREDICATED_TILING | FT_DONT_RESIZE;

	CTexture::s_ptexStereoL = CTexture::CreateRenderTarget( "$StereoL", nWidth, nHeight, eTT_2D, nFlags, eTF_A8R8G8B8 );
	CTexture::s_ptexStereoR = CTexture::CreateRenderTarget( "$StereoR", nWidth, nHeight, eTT_2D, nFlags, eTF_A8R8G8B8 );

	m_pLeftTex = CTexture::s_ptexStereoL;
	m_pRightTex = CTexture::s_ptexStereoR;





}


void CD3DStereoRenderer::Shutdown()
{
	ReleaseResources();






#ifdef USE_NV_API
	if( m_nvStereoHandle )
	{
		NvAPI_Stereo_DestroyHandle( m_nvStereoHandle );
		m_nvStereoHandle = NULL;
	}
#endif
}


void CD3DStereoRenderer::ReleaseResources()
{
	if( m_device == STEREO_DEVICE_NONE )
		return;

	SAFE_RELEASE( CTexture::s_ptexStereoL );
	SAFE_RELEASE( CTexture::s_ptexStereoR );
}


bool CD3DStereoRenderer::EnableStereo()
{
#ifdef USE_NV_API
	if( IsDriver( DRIVER_NV ) )
	{
		uint8 windowedSupport = 0;
		NvAPI_Stereo_IsWindowedModeSupported( &windowedSupport );
		
		if( m_renderer.m_bFullScreen || windowedSupport )
		{
			NvAPI_Stereo_Activate( m_nvStereoHandle );
			NvAPI_Stereo_SetNotificationMessage( m_nvStereoHandle, NULL, NULL );
			NvAPI_Stereo_SetActiveEye( m_nvStereoHandle, NVAPI_STEREO_EYE_LEFT );
			return true;
		}
		else
		{
			NvAPI_Stereo_Deactivate( m_nvStereoHandle );
			iLog->LogError( "3DVision requires fullscreen mode" );
			return false;
		}
	}
	else
	{
		// The cursor needs to be rendered on both eyes
		if( !gEnv->IsEditor() && gEnv->pHardwareMouse )
			gEnv->pHardwareMouse->UseSystemCursor( false );
	}
#endif





























	return true;
}


void CD3DStereoRenderer::DisableStereo()
{
#ifdef USE_NV_API
	if( gEnv->pHardwareMouse )
		gEnv->pHardwareMouse->UseSystemCursor( true );
	
	if( IsDriver( DRIVER_NV ) )
	{
		NvAPI_Stereo_Deactivate( m_nvStereoHandle );
	}
#endif



















	m_gammaAdjustment = 0;
}


void CD3DStereoRenderer::ChangeOutputFormat()
{
	m_frontBufWidth = 0;
	m_frontBufHeight = 0;























}


void CD3DStereoRenderer::PrepareStereo( StereoMode mode, StereoOutput output )
{
#ifdef USE_NV_API
	if( IsDriver( DRIVER_NV ) )
	{
		HandleNVControl();
	}
#endif
	
	if( m_mode != mode || m_output != output )
	{
		m_renderer.ForceFlushRTCommands();
		
		if( m_mode != mode )
		{
			m_mode = mode;
			m_output = output;
			
			if( mode == STEREO_MODE_NO_STEREO )
			{
				DisableStereo();
			}
			else
			{
				EnableStereo();
				ChangeOutputFormat();
			}
		}
		else if( m_output != output )
		{
			m_output = output;
			
			if( IsStereoEnabled() )
				ChangeOutputFormat();
		}
	}

#if defined(WIN32) || defined(WIN64) || defined(gringo)
	//if( IsStereoEnabled() )
	{
		// Check if resolution changed on PC
		if( CTexture::s_ptexStereoL && CTexture::s_ptexStereoR )
		{
			if( m_renderer.GetWidth() != CTexture::s_ptexStereoL->GetWidth() ||
			    m_renderer.GetHeight() != CTexture::s_ptexStereoL->GetHeight() )
			{
				m_renderer.ForceFlushRTCommands();
				CreateIntermediateBuffers();
			}
		}
	}
#endif

	if( IsStereoEnabled() )
	{
		// Update stereo parameters
		m_stereoStrength = CRenderer::CV_r_StereoStrength;
		m_maxSeparationScene = CRenderer::CV_r_StereoEyeDist;
		m_zeroParallaxPlaneDist = CRenderer::CV_r_StereoScreenDist;
		m_nearGeoScale = CRenderer::CV_r_StereoNearGeoScale;
		m_gammaAdjustment = CRenderer::CV_r_StereoGammaAdjustment;

		// Apply stereo strength
		m_maxSeparationScene *= m_stereoStrength;
	}
}


void CD3DStereoRenderer::HandleNVControl()
{
#ifdef USE_NV_API
	if( m_stereoStrength != CRenderer::CV_r_StereoStrength )
	{
		NvAPI_Stereo_SetSeparation( m_nvStereoHandle, clamp_tpl( (CRenderer::CV_r_StereoStrength - 0.5f) * 100.f, 0.f, 100.f ) );
	}
	
	NvAPI_Stereo_IsActivated( m_nvStereoHandle, &m_nvStereoActivated );
	NvAPI_Stereo_GetSeparation( m_nvStereoHandle, &m_nvStereoStrength );
#endif
}


void CD3DStereoRenderer::Update()
{
	if( m_device != STEREO_DEVICE_NONE )
	{
		m_renderer.m_pRT->RC_PrepareStereo(CRenderer::CV_r_StereoMode, CRenderer::CV_r_StereoOutput);
	}
}


void CD3DStereoRenderer::ProcessScene(int sceneFlags)
{
	int nThreadID = m_renderer.m_RP.m_nFillThreadID;

	if (CRenderer::CV_r_StereoMode == STEREO_MODE_DUAL_RENDERING)
	{
		CCamera cam = m_renderer.m_RP.m_TI[nThreadID].m_cam;
		
		float fNear = cam.GetNearPlane();
		float maxSeparation = CRenderer::CV_r_StereoEyeDist;
		float screenDist = CRenderer::CV_r_StereoScreenDist;
		float z = 99999.0f;  // Point which is far away

		float wT = tanf( cam.GetFov() * 0.5f ) * fNear;
		float wR = wT * cam.GetProjRatio(), wL = -wR;
		float p00 = 2 * fNear / (wR - wL);
		float p02 = (wL + wR) / (wL - wR);

		// Compute required camera shift, so that a distant point gets the desired maximum separation
		float camOffset = (maxSeparation - p02) / (p00/z - p00/screenDist);
		float frustumShift = camOffset * (fNear / screenDist);

		// Left eye
		{
			m_renderer.PushProfileMarker("LEFT_EYE");
			CCamera camL = cam;
			Matrix34 stereoMat = Matrix34::CreateTranslationMat( Vec3( -camOffset, 0, 0 ) );
			camL.SetMatrix( camL.GetMatrix() * stereoMat );
			camL.SetAsymmetry( frustumShift, frustumShift, 0, 0 );
			m_renderer.SetCamera(camL);

			RenderScene(sceneFlags);
			CopyToStereoFromMainThread(0);
			m_renderer.PopProfileMarker("LEFT_EYE");
		}

		// Right eye
		{
			m_renderer.PushProfileMarker("RIGHT_EYE");
			CCamera camR = cam;
			Matrix34 stereoMat = Matrix34::CreateTranslationMat( Vec3( camOffset, 0, 0 ) );
			camR.SetMatrix( camR.GetMatrix() * stereoMat );
			camR.SetAsymmetry( -frustumShift, -frustumShift, 0, 0 );
			m_renderer.SetCamera(camR);

			RenderScene(sceneFlags);
			CopyToStereoFromMainThread(1);
			m_renderer.PopProfileMarker("RIGHT_EYE");
		}
	}
	else
	{
		RenderScene(sceneFlags);
	}
}


void CD3DStereoRenderer::CopyToStereo(int channel)
{
	assert(IsRenderThread());

	PROFILE_LABEL_PUSH("COPY_TO_STEREO");

	CTexture *pTex;

	if( channel == 0 )
	{
		pTex = CTexture::s_ptexStereoL;
		m_needClearLeft = false;
	}
	else
	{
		pTex = CTexture::s_ptexStereoR;
		m_needClearRight = false;
	}

	if (pTex==NULL)
		return;

	GetUtils().CopyScreenToTexture( pTex );

	PROFILE_LABEL_POP("COPY_TO_STEREO");
}


void CD3DStereoRenderer::DisplayStereo()
{
	assert(IsRenderThread());
	  
	if (!IsStereoEnabled() || m_renderer.m_bDeviceLost)  // When unloading level, m_bDeviceLost is set to 2
		return;

#if PS3
	if( m_device == STEREO_DEVICE_HDMI )
		return;
#endif

	ResolveStereoBuffers();
	m_needClearLeft = true;
	m_needClearRight = true;

	m_renderer.m_cEF.mfRefreshSystemShader("Stereo", CShaderMan::m_ShaderStereo);

	PROFILE_LABEL_PUSH("DISPLAY_STEREO");
















	
	m_renderer.Set2DMode( true, 1, 1 );

	if (m_renderer.m_pSecondBackBuffer != 0)
	{
		m_renderer.FX_PushRenderTarget(1, m_renderer.m_pSecondBackBuffer, NULL);
		m_renderer.RT_SetViewport(0, 0, m_renderer.m_NewViewport.nWidth, m_renderer.m_NewViewport.nHeight);
	}

	CShader *pSH = m_renderer.m_cEF.m_ShaderStereo;
	SelectShaderTechnique();

	uint32 nPasses = 0;
	pSH->FXBegin(&nPasses, FEF_DONTSETSTATES);
	pSH->FXBeginPass(0);

	m_renderer.FX_SetState(GS_NODEPTHTEST);

  int width = m_renderer.GetWidth();
  int height = m_renderer.GetHeight();
  
  Vec4 pParams= Vec4((float) width, (float) height, 0, 0);
  CShaderMan::m_shPostEffects->FXSetPSFloat(m_SourceSizeParamName, &pParams, 1);  

	GetUtils().SetTexture(!CRenderer::CV_r_StereoFlipEyes ? GetLeftEye() : GetRightEye(), 0, FILTER_LINEAR);
	GetUtils().SetTexture(!CRenderer::CV_r_StereoFlipEyes ? GetRightEye() : GetLeftEye(), 1, FILTER_LINEAR);
	
	GetUtils().DrawFullScreenQuad(width, height);

#ifdef USE_NV_API
	if( IsDriver( DRIVER_NV ) )
	{
		NvAPI_Stereo_SetActiveEye( m_nvStereoHandle, NVAPI_STEREO_EYE_RIGHT );
		GetUtils().SetTexture( !CRenderer::CV_r_StereoFlipEyes ? GetRightEye() : GetLeftEye(), 0, FILTER_LINEAR );
		GetUtils().DrawFullScreenQuad( width, height );
		NvAPI_Stereo_SetActiveEye( m_nvStereoHandle, NVAPI_STEREO_EYE_LEFT );
	}
#endif

	pSH->FXEndPass();
	pSH->FXEnd();

	m_renderer.Set2DMode( false, 1, 1 );
	
	if (m_renderer.m_pSecondBackBuffer != 0)
		m_renderer.FX_PopRenderTarget(1);

	PROFILE_LABEL_POP("DISPLAY_STEREO");

	RestoreResources();
}


void CD3DStereoRenderer::BeginRenderingMRT(bool disableClear)
{
	if (!IsStereoEnabled())
		return;

	if( disableClear )
	{
		m_needClearLeft = false;
		m_needClearRight = false;
	}

	PatchResources();
	PushRenderTargets();
}


void CD3DStereoRenderer::EndRenderingMRT(bool bResolve)
{
	if (!IsStereoEnabled())
		return;

  PopRenderTargets(bResolve);
}


void CD3DStereoRenderer::TakeScreenshot(const char path[])
{
	stack_string pathL(path);
	stack_string pathR(path);

	pathL.insert(pathL.find('.'), "_L");
	pathR.insert(pathR.find('.'), "_R");

	((CD3D9Renderer*) gRenDev)->CaptureFrameBufferToFile(pathL.c_str(), GetLeftEye(), 1.0f);
	((CD3D9Renderer*) gRenDev)->CaptureFrameBufferToFile(pathR.c_str(), GetRightEye(), 1.0f);
}


void CD3DStereoRenderer::ResolveStereoBuffers()
{








}


void CD3DStereoRenderer::BeginRenderingTo(StereoEye eye)
{
	m_curEye = eye;











	
	PatchResources();
	
	if (eye == LEFT_EYE)
	{
		gRenDev->SetProfileMarker("LEFT_EYE", CRenderer::ESPM_PUSH);
		GetLeftEye()->SetRenderTargetTile(0);
		m_renderer.FX_PushRenderTarget(0, GetLeftEye(), &m_renderer.m_DepthBufferOrigMSAA);
		if( m_needClearLeft )
		{
			ColorF clearColor(0, 0, 0, 0);
			m_renderer.EF_ClearBuffers( FRT_CLEAR_COLOR | FRT_CLEAR_IMMEDIATE, &clearColor );
			m_needClearLeft = false;
		}
	}
	else
	{
		gRenDev->SetProfileMarker("RIGHT_EYE", CRenderer::ESPM_PUSH);
		GetRightEye()->SetRenderTargetTile(1);
		m_renderer.FX_PushRenderTarget(0, GetRightEye(), &m_renderer.m_DepthBufferOrigMSAA);
		if( m_needClearRight )
		{
			ColorF clearColor(0, 0, 0, 0);
			m_renderer.EF_ClearBuffers( FRT_CLEAR_COLOR | FRT_CLEAR_IMMEDIATE, &clearColor );
			m_needClearRight = false;
		}
	}

	m_renderer.FX_SetActiveRenderTargets();

  if (eye == LEFT_EYE)
    GetLeftEye()->SetResolved(true);
  else
    GetRightEye()->SetResolved(true);
}


void CD3DStereoRenderer::EndRenderingTo(StereoEye eye)
{








	if (eye == LEFT_EYE)
	{
		gRenDev->SetProfileMarker("LEFT_EYE", CRenderer::ESPM_POP);
		GetLeftEye()->SetResolved(true);
		m_renderer.FX_PopRenderTarget(0);
		GetLeftEye()->SetRenderTargetTile(0);
	}
	else
	{
		gRenDev->SetProfileMarker("RIGHT_EYE", CRenderer::ESPM_POP);
		GetRightEye()->SetResolved(true);
		m_renderer.FX_PopRenderTarget(0);
		GetRightEye()->SetRenderTargetTile(0);
	}
}


void CD3DStereoRenderer::PatchResources()
{
	// On consoles we reuse the light accumulation buffers as stereo RTs to save memory
	// On the PS3 the format matches already, on 360 it needs to be patched to RGBA8
	// On PC/D3D9 this is not possible as the RT texture descriptor can't be changed on the fly

















































}


void CD3DStereoRenderer::RestoreResources()
{























}


void CD3DStereoRenderer::SelectShaderTechnique()
{
	gRenDev->m_cEF.mfRefreshSystemShader("Stereo", CShaderMan::m_ShaderStereo);

  CShader *pSH = m_renderer.m_cEF.m_ShaderStereo;

	if( m_device == STEREO_DEVICE_FRAMECOMP )
	{
		switch(GetStereoOutput())
		{
		case STEREO_OUTPUT_CHECKERBOARD:
			pSH->FXSetTechnique("Checkerboard");
			break;
		case STEREO_OUTPUT_SIDE_BY_SIDE:
			pSH->FXSetTechnique("SideBySide");
			break;
		case STEREO_OUTPUT_LINE_BY_LINE:
			pSH->FXSetTechnique("LineByLine");
			break;
	#ifndef _RELEASE	
		case STEREO_OUTPUT_ANAGLYPH:
			pSH->FXSetTechnique("Anaglyph");
			break;
	#endif
		default:
			pSH->FXSetTechnique("Emulation");
		}
	}
	else if( IsDriver( DRIVER_NV ) )
	{
		pSH->FXSetTechnique("NV3DVision");
	}
	else if( m_device == STEREO_DEVICE_DUALHEAD )
	{
		switch(GetStereoOutput())
		{		
		case STEREO_OUTPUT_STANDARD:
			pSH->FXSetTechnique("DualHead");
			break;
		case STEREO_OUTPUT_IZ3D:
			pSH->FXSetTechnique("IZ3D");
			break;
		default:
			pSH->FXSetTechnique("Emulation");
		}
	}
	else
	{
		pSH->FXSetTechnique("Emulation");
	}
}


void CD3DStereoRenderer::RenderScene(int sceneFlags)
{
	m_renderer.EF_Scene3D(m_renderer.m_MainRTViewport, sceneFlags);
}


bool CD3DStereoRenderer::IsRenderThread() const
{
	return m_renderer.m_pRT->IsRenderThread();
}


void CD3DStereoRenderer::CopyToStereoFromMainThread(int channel)
{
	m_renderer.m_pRT->RC_CopyToStereoTex(channel);
}


void CD3DStereoRenderer::PushRenderTargets()
{











  GetLeftEye()->SetRenderTargetTile(0);
  GetRightEye()->SetRenderTargetTile(0);

  m_renderer.FX_PushRenderTarget(0, GetLeftEye(),  &gcpRendD3D->m_DepthBufferOrigMSAA); 
  m_renderer.FX_PushRenderTarget(1, GetRightEye(), NULL);

  m_renderer.RT_SetViewport(0, 0, GetLeftEye()->GetWidth(), GetLeftEye()->GetHeight());

  m_renderer.FX_SetActiveRenderTargets();

	if( m_needClearLeft || m_needClearRight )
	{
		ColorF clearColor(0, 0, 0, 0);
		m_renderer.EF_ClearBuffers( FRT_CLEAR_COLOR | FRT_CLEAR_IMMEDIATE, &clearColor );
		m_needClearLeft = false;
		m_needClearRight = false;
	}
}


void CD3DStereoRenderer::PopRenderTargets(bool bResolve)
{





  m_renderer.FX_PopRenderTarget(1); 
  m_renderer.FX_PopRenderTarget(0);
}


////////////////////////////////////////////////////////////////////////////////////////////////////
// IStereoRenderer Interface
////////////////////////////////////////////////////////////////////////////////////////////////////

StereoDevice CD3DStereoRenderer::GetDevice()
{
	return m_device;
}


StereoDeviceState CD3DStereoRenderer::GetDeviceState()
{
	return m_deviceState;
}


bool CD3DStereoRenderer::GetStereoEnabled()
{
	return IsStereoEnabled();
}


float CD3DStereoRenderer::GetStereoStrength()
{
	return m_stereoStrength;
}


float CD3DStereoRenderer::GetMaxSeparationScene(bool half)
{
	return m_maxSeparationScene * (half ? 0.5f : 1.0f);
}


float CD3DStereoRenderer::GetZeroParallaxPlaneDist()
{
	return m_zeroParallaxPlaneDist;
}


void CD3DStereoRenderer::GetNVControlValues(bool &stereoActivated, float &stereoStrength)
{
	stereoActivated = m_nvStereoActivated != 0;
	stereoStrength = m_nvStereoStrength;
}
